核心层开发:useChat与流式状态管理
上一节我们设计了组件库的架构和类型系统,这节开始写
@ai-chat/core的核心代码。core 包是整个组件库的心脏——它不依赖 Vue,不依赖任何框架,只做一件事:管理聊天状态和流式数据。
core 包的职责边界
core 包的代码量不大,但每一个函数都有明确的职责:
| 导出 | 职责 | 框架依赖 |
|---|---|---|
useChat | 管理聊天状态(消息列表、流式状态、发送/中断/清空) | Vue reactive |
generateId | 生成唯一消息ID | 无 |
createUserMessage | 创建用户消息对象 | 无 |
createAssistantMessage | 创建助手消息对象 | 无 |
| 类型导出 | Message、StreamChunk、ChatAdapter 等 | 无 |
注意:useChat 是 core 包中唯一依赖 Vue 的函数(用了 reactive)。这个设计看似违反了"core 不依赖 Vue"的原则,但实际是合理的——core 包的目标用户就是 Vue 开发者,reactive 是 Vue 3 的运行时API,不是模板编译期依赖。
如果你需要纯框架无关的状态管理,可以把 useChat 拆到一个单独的 @ai-chat/vue-composables 包中。但在实际项目中,这个拆分的收益不大——99% 的 core 包用户都会同时使用 Vue。
useChat:核心 Composable 的完整实现
函数签名
function useChat(
adapter: ChatAdapter,
options?: ChatOptions
): UnwrapNestedRefs<ChatState>
typescript
两个参数:
adapter:后端适配器,必需。决定了数据从哪来options:配置项,可选。包括初始消息、错误回调、流式回调、历史消息上限
返回值是 ChatState 的响应式代理——组件可以直接绑定到模板中使用。
内部状态初始化
const state = reactive<ChatState>({
messages: options?.initialMessages ? [...options.initialMessages] : [],
isStreaming: false,
error: null,
send,
abort,
clear,
})
let currentController: AbortController | null = null
typescript
两个要点:
initialMessages用展开运算符复制,避免外部修改影响内部状态currentController不放在 reactive state 中——它是内部实现细节,不需要暴露给组件
send 函数:流式消息发送的核心流程
send 函数是整个 core 包最核心的逻辑,它的执行流程:
第一步:创建用户消息和空助手消息
const userMessage = createUserMessage(content, attachments)
state.messages.push(userMessage)
const assistantMessage = createAssistantMessage()
state.messages.push(assistantMessage)
typescript
先添加用户消息,再添加一个内容为空的助手消息。助手消息的 content 在流式过程中逐步填充——这就是流式渲染的基础。
第二步:创建 AbortController 并调用适配器
currentController = new AbortController()
const stream = adapter.sendMessage({
messages: state.messages.slice(0, -1),
signal: currentController.signal,
})
typescript
messages.slice(0, -1) 排除了刚添加的空助手消息——发送给后端的是纯对话历史。signal 用于支持中断操作。
第三步:消费流式数据
for await (const chunk of stream) {
if (chunk.type === 'done') break
if (chunk.type === 'error') {
state.error = new Error(chunk.content)
options?.onError?.(state.error)
break
}
if (chunk.type === 'text') {
assistantMessage.content += chunk.content
}
options?.onResponse?.(chunk)
}
typescript
for await...of 消费 AsyncGenerator,每收到一个 chunk 就追加到助手消息的 content 上。因为 state 是 reactive 的,Vue 会自动追踪 assistantMessage.content 的变化,触发视图更新——组件不需要做任何额外处理。
三种 chunk 类型的处理:
done:流结束,跳出循环error:记录错误,触发回调,跳出循环text:追加文本内容
第四步:错误处理
catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
state.error = err
options?.onError?.(err)
}
} finally {
state.isStreaming = false
currentController = null
}
typescript
AbortError 是用户主动中断,不算错误,静默忽略。其他错误才记录并回调。finally 确保无论成功还是失败,都重置流式状态。
abort 函数:中断流式请求
function abort(): void {
if (currentController) {
currentController.abort()
state.isStreaming = false
}
}
typescript
调用 AbortController 的 abort 方法,后端适配器在收到 signal 的 abort 事件后应该停止生成。isStreaming 立即设为 false,UI 可以立即响应。
clear 函数:清空对话
function clear(): void {
state.messages = []
state.error = null
state.isStreaming = false
}
typescript
重置所有状态。如果正在流式中调用 clear,因为 currentController 没有被 abort,后端可能还会继续生成。这是一个已知的边界情况——如果你需要在 clear 时也中断请求,可以在 clear 中先调用 abort。
maxHistory:消息历史上限
if (options?.maxHistory) {
const max = options.maxHistory
const originalPush = state.messages.push.bind(state.messages)
state.messages.push = function (...items: Message[]) {
const result = originalPush(...items)
while (state.messages.length > max) {
state.messages.shift()
}
return result
}
}
typescript
通过代理 messages.push 方法,在每次添加消息后检查长度,超出上限就移除最早的消息。这样组件不需要手动管理消息队列长度。
工具函数
generateId
let counter = 0
export function generateId(): string {
return `msg_${Date.now()}_${++counter}`
}
typescript
组合时间戳和递增计数器,确保唯一性。为什么不用 UUID?因为 msg_ 前缀在调试时一眼就能看出是消息ID,比随机字符串更有辨识度。
createUserMessage / createAssistantMessage
export function createUserMessage(
content: string,
attachments?: Attachment[]
): Message {
return {
id: generateId(),
role: 'user',
content,
attachments,
createdAt: new Date(),
}
}
export function createAssistantMessage(
content = '',
metadata?: Record<string, unknown>
): Message {
return {
id: generateId(),
role: 'assistant',
content,
metadata,
createdAt: new Date(),
}
}
typescript
两个工厂函数,封装了消息创建的细节。外部代码不需要知道 ID 怎么生成、时间戳怎么处理。
ChatAdapter 实现模式
core 包定义了 ChatAdapter 接口,但不需要提供实现——实现由使用者根据具体后端编写。不过,一个最简的 Adapter 示例能帮助理解:
import type { ChatAdapter } from '@ai-chat/core'
const openaiAdapter: ChatAdapter = {
async *sendMessage({ messages, signal }) {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'gpt-5-mini',
messages: messages.map(m => ({ role: m.role, content: m.content })),
stream: true,
}),
signal,
})
const reader = res.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value, { stream: true })
// 解析 SSE 格式并 yield StreamChunk
for (const line of text.split('\n')) {
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
const json = JSON.parse(line.slice(6))
const content = json.choices[0]?.delta?.content
if (content) yield { type: 'text', content }
}
}
}
yield { type: 'done', content: '' }
},
}
typescript
三个要点:
async *sendMessage语法——既是异步函数又是生成器- 从 fetch Response 的 ReadableStream 中读取数据
- 解析 SSE(Server-Sent Events)格式,yield StreamChunk
用 AI 写 core 层代码的提示词技巧
core 层的代码逻辑清晰但细节多(流式处理、状态管理、错误处理),非常适合用 AI 辅助编写。提示词的关键是给足上下文:
基于以下类型定义,实现 useChat composable:
- 输入:ChatAdapter 实例 + ChatOptions 配置
- 输出:reactive ChatState
- 功能要求:send(流式发送)、abort(中断)、clear(清空)、maxHistory 上限
- 错误处理:区分 AbortError 和真实错误
text
把类型定义(Message、StreamChunk、ChatAdapter 等)一起粘贴进去。AI 能直接生成完整的实现代码,你只需要审查流式处理和错误处理的逻辑是否正确。
本章小结
- core 包的
useChat是整个组件库的心脏,管理聊天状态和流式数据 - send 函数的流程:创建消息 → 调用适配器 → for await 消费流 → 处理 chunk → 错误恢复
- AbortController 贯穿整个流式生命周期,支持用户随时中断
- ChatAdapter 用
async *function实现,yield StreamChunk - 工具函数(generateId、createUserMessage)封装创建细节,保持外部代码简洁
下一节我们开始写 Vue 组件:ChatWindow、MessageList、MessageBubble 和 StreamText。
↑